コンセプトから学ぶAmazon DynamoDB【LSI篇】
よく訓練されたアップル信者、都元です。今回はローカル・セカンダリ・インデックス(LSI)について深堀りしてみましょう。
ここまでの復習
その前に、色々復習しておきましょう。
DynamoDBのインデックス3種類
まず、DynamoDBが持てるインデックス3種類をあらためてまとめておきます。
- 暗黙的な、キー(ハッシュキーのみ、または、ハッシュキー+レンジキー)による検索のためのインデックス
- 明示的に定義した、ローカル・セカンダリ・インデックス(LSI)
- 明示的に定義した、グローバル・セカンダリ・インデックス(GSI)
このうち、暗黙的なインデックスは1テーブルにつき1つだけ、LSI及びGSIは1テーブルにつき各5個まで定義できます。つまり、1つのテーブルには最小で1個、最大で11個のインデックスがあります。検索を行う場合は、まず「どのインデックスを使って検索をするか」を指定し、その上で検索条件を指定します。具体的には後ほど。
ちなみに上記「暗黙的なインデックス」のことを、シンプルに「テーブル」と呼ぶこともある、と覚えておいてください。具体的には「テーブルに対するクエリ」と言えば、この暗黙的インデックスを使った検索です。一方、各LSI及びGSIには固有の名前が付いており、「(その名前)に対するクエリ」という表現で、GSIまたはLSIを使ったクエリであることを表します。
複合キーテーブルとパーティション
復習になりますが、複合キーテーブルでは、キーとして「ハッシュキー」「レンジキー」の2つを指定します。DynamoDBではパーティションと呼ばれる複数のノードにデータを分散配置することによって、性能を確保しています。このパーティション分けの拠り所となるのがハッシュキーの値です。
つまり、同じハッシュキーの値を持つアイテムは、同じパーティションに保存されます。そして、これらのアイテムは、レンジキーによってソートされた状態で保存されています、多分。(実装は非公開のため、予想です)
とある1つのパーティションの中で、レンジキーによるインデックスがあると考えても良いと思います。詳しくはこの辺りを御覧ください。
ローカル・セカンダリ・インデックス(LSI)とは
さて、今回はLSIにフォーカスします。前提として、LSIは複合キーテーブルにしか定義できません。つまり、ハッシュキーテーブルでは利用できないインデックスです。
以前コンセプトから学ぶAmazon DynamoDB【複合キーテーブル篇】において、Replay
というサンプルのテーブルを示しました。これは、Id
がハッシュキー、ReplyDateTime
がレンジキーとなる複合キーテーブルです。
// Reply { "Id": "DynamoDB#DynamoDB Thread 1", "ReplyDateTime": "2011-12-11T00:40:57.165Z", "Message": "DynamoDB Thread 1 Reply 1 text", "PostedBy": "User A" } { "Id": "DynamoDB#DynamoDB Thread 1", "ReplyDateTime": "2011-12-18T00:40:57.165Z", "Message": "DynamoDB Thread 1 Reply 1 text", "PostedBy": "User A" } (略)
このテーブルはキーによる暗黙的なインデックスによって「DynamoDB#DynamoDB Thread 1
というスレッドの、2011-12-25
の返信を全部取り出したい」といったクエリを高速に実行できます。(下記サンプルは、本稿末尾のサンプルデータテーブルの作成及びアイテムのインポートを行った状態で実行できます。)
$ aws dynamodb query \ --table-name Reply \ --key-condition-expression "Id = :id AND begins_with(ReplyDateTime, :date)" \ --expression-attribute-values '{":id":{"S":"DynamoDB#DynamoDB Thread 1"},":date":{"S":"2011-12-25"}}' { "Count": 1, "Items": [ { "Message": { "S": "DynamoDB Thread 1 Reply 3 text" }, "PostedBy": { "S": "User B" }, "ReplyDateTime": { "S": "2011-12-25T00:40:57.165Z" }, "Id": { "S": "DynamoDB#DynamoDB Thread 1" } } ], "ScannedCount": 1, "ConsumedCapacity": null }
しかしこれと同時に「DynamoDB#DynamoDB Thread 1
というスレッドの、User B
による返信を全部取り出したい」というアプリケーション要件も存在する場合を考えてみましょう。このようなときに活躍するのがLSIです。
このテーブルでは、暗黙的なインデックスとは別に、Id
がハッシュキー、PostedBy
がレンジキーとなるLSI「PostedBy-index」を定義し構築してあります。このインデックスを使えば「DynamoDB#DynamoDB Thread 1
というスレッドの、User B
による返信を全部取り出したい」いったクエリも高速に実行できるようになります。
この「PostedBy-index」に対する(≠暗黙的なインデックスに対する)問い合わせ例は下記の通りです。(このサンプルは、本稿末尾のサンプルデータテーブルの作成及びアイテムのインポートを行った状態で実行できます。)
$ aws dynamodb query \ --table-name Reply \ --index-name PostedBy-index \ --key-condition-expression "Id = :id AND PostedBy = :by" \ --expression-attribute-values '{":id":{"S":"DynamoDB#DynamoDB Thread 1"},":by":{"S":"User B"}}' { "Count": 1, "Items": [ { "PostedBy": { "S": "User B" }, "Id": { "S": "DynamoDB#DynamoDB Thread 1" }, "ReplyDateTime": { "S": "2011-12-25T00:40:57.165Z" } } ], "ScannedCount": 1, "ConsumedCapacity": null }
何が「ローカル」なのか?
前述の通り、LSIは複合キーテーブルにしか定義できません。そしてLSIは、そのテーブルのハッシュキーと同じハッシュキーでしか定義できません。つまり、PostedBy
をハッシュキー、Id
をレンジキーとなるLSIを定義はできません。ハッシュキーは必ずId
である必要があります。
これは何を意味するのかというと。同じパーティション内の複数のアイテムは、基本的に暗黙的なインデックスに従って整理されています。これに加えて、別の規則(ここではPostedBy
)で整理インデックスを構築する、これがLSIです。
つまり「ローカル」というのは「同じパーティション内」ということを表した言葉です。
インデックスへの射影(プロジェクション)
上記、テーブル(暗黙インデックス)に対する問い合わせではMessage
attributeが含まれていたのに、PostedBy-indexに対する問い合わせではMessage
が含まれていません。なぜでしょうか。
テーブルには「全てのattribute」が含まれています。これは当たり前で、そして直感的だと思います。
一方、インデックスには「そのインデックスのキーとなるattribute (index key attributes)」及び「テーブルのキーattribute(primary key attributes)」 が最低限全て含まれますが、それ以外のattributeについては含まない可能性があります。言い換えると、インデックスにはテーブルのattributeの一部しか射影されていません。
本稿で挙げた例は、最低限のattributeのみが射影されたインデックスです。つまり、PostedBy-indexにはId
, PostedBy
, ReplayDateTime
という3つのattributeしか含まれていません。仮にMessage
が欲しければ、PostedBy-indexに対する問い合わせをした後、得られたテーブルのキー(Id
及びReplayDateTime
)を使って、テーブルに対する問い合わせを行います。
インデックスに対する問い合わせの結果、一発でMessage
が得られれば楽なのに、なぜこのような設定ができるのか。これにはインデックス容量の削減という効果があります。
実はLSIには「同じハッシュキーを持つitem」の合計サイズに、10GBという制限があります。便利だからといって、全てのattributeをインデックスに射影していると、この上限にぶつかる可能性が出てきます。10GBの壁への到達を防止するために、必要最低限のattributeだけを射影するように、LSIを設計する必要があるのです。
付録:サンプルデータの投入
本稿で利用するサンプルデータは、AWS CLIのコマンドでは下記のように投入してください。
参考:Amazon DynamoDB Developer Guide - サンプルテーブルとデータ
aws dynamodb create-table \ --table-name Forum \ --attribute-definitions AttributeName=Name,AttributeType=S \ --key-schema AttributeName=Name,KeyType=HASH \ --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 aws dynamodb put-item \ --table-name Forum \ --item '{"Name":{"S":"DynamoDB"},"Category":{"S":"Amazon Web Services"},"Threads":{"N":"3"},"Messages":{"N":"4"},"Views":{"N":"1000"},"LastPostBy":{"S":"User A"},"LastPostDateTime":{"S":"2012-01-03T00:40:57.165Z"}}' aws dynamodb put-item \ --table-name Forum \ --item '{"Name":{"S":"Amazon S3"},"Category":{"S":"AWS"},"Threads":{"N":"1"}}'
aws dynamodb create-table \ --table-name Thread \ --attribute-definitions AttributeName=ForumName,AttributeType=S AttributeName=Subject,AttributeType=S \ --key-schema AttributeName=ForumName,KeyType=HASH AttributeName=Subject,KeyType=RANGE \ --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 aws dynamodb put-item \ --table-name Thread \ --item '{"ForumName":{"S":"DynamoDB"},"Subject":{"S":"DynamoDB Thread 1"},"Message":{"S":"DynamoDB thread 1 message text"},"LastPostedBy":{"S":"User A"},"Views":{"N":"0"},"Replies":{"N":"0"},"Answered":{"N":"0"},"Tags":{"SS":["index","primarykey","table"]},"LastPostDateTime":{"S":"2012-01-03T00:40:57.165Z"}}' aws dynamodb put-item \ --table-name Thread \ --item '{"ForumName":{"S":"DynamoDB"},"Subject":{"S":"DynamoDB Thread 2"},"Message":{"S":"DynamoDB thread 2 message text"},"LastPostedBy":{"S":"User A"},"Views":{"N":"0"},"Replies":{"N":"0"},"Answered":{"N":"0"},"Tags":{"SS":["index","primarykey","rangekey"]},"LastPostDateTime":{"S":"2012-01-03T00:40:57.165Z"}}' aws dynamodb put-item \ --table-name Thread \ --item '{"ForumName":{"S":"Amazon S3"},"Subject":{"S":"Amazon S3 Thread 1"},"Message":{"S":"Amazon S3 Thread 1 message text"},"LastPostedBy":{"S":"User A"},"Views":{"N":"0"},"Replies":{"N":"0"},"Answered":{"N":"0"},"Tags":{"SS":["largeobject","multipart upload"]},"LastPostDateTime":{"S":"2012-01-03T00:40:57.165Z"}}'
aws dynamodb create-table \ --table-name Reply \ --attribute-definitions AttributeName=Id,AttributeType=S AttributeName=ReplyDateTime,AttributeType=S AttributeName=PostedBy,AttributeType=S \ --key-schema AttributeName=Id,KeyType=HASH AttributeName=ReplyDateTime,KeyType=RANGE \ --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \ --local-secondary-indexes "IndexName=PostedBy-index,KeySchema=[{AttributeName=Id,KeyType=HASH},{AttributeName=PostedBy,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}" aws dynamodb put-item \ --table-name Reply3 \ --item '{"Id":{"S":"DynamoDB#DynamoDB Thread 1"},"ReplyDateTime":{"S":"2011-12-11T00:40:57.165Z"},"Message":{"S":"DynamoDB Thread 1 Reply 1 text"},"PostedBy":{"S":"User A"}}' aws dynamodb put-item \ --table-name Reply \ --item '{"Id":{"S":"DynamoDB#DynamoDB Thread 1"},"ReplyDateTime":{"S":"2011-12-18T00:40:57.165Z"},"Message":{"S":"DynamoDB Thread 1 Reply 1 text"},"PostedBy":{"S":"User A"}}' aws dynamodb put-item \ --table-name Reply \ --item '{"Id":{"S":"DynamoDB#DynamoDB Thread 1"},"ReplyDateTime":{"S":"2011-12-25T00:40:57.165Z"},"Message":{"S":"DynamoDB Thread 1 Reply 3 text"},"PostedBy":{"S":"User B"}}' aws dynamodb put-item \ --table-name Reply \ --item '{"Id":{"S":"DynamoDB#DynamoDB Thread 2"},"ReplyDateTime":{"S":"2011-12-25T00:40:57.165Z"},"Message":{"S":"DynamoDB Thread 2 Reply 1 text"},"PostedBy":{"S":"User A"}}' aws dynamodb put-item \ --table-name Reply \ --item '{"Id":{"S":"DynamoDB#DynamoDB Thread 2"},"ReplyDateTime":{"S":"2012-01-03T00:40:57.165Z"},"Message":{"S":"DynamoDB Thread 2 Reply 2"},"PostedBy":{"S":"User A"}}'
また、今回は使いませんが、ProductCatalogのサンプルも置いておきます。
aws dynamodb create-table \ --table-name ProductCatalog \ --attribute-definitions AttributeName=Id,AttributeType=N \ --key-schema AttributeName=Id,KeyType=HASH \ --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 aws dynamodb put-item \ --table-name ProductCatalog \ --item '{"Id":{"N":"101"},"Title":{"S":"Book101Title"},"ISBN":{"S":"111-1111111111"},"Authors":{"SS":["Author1"]},"Price":{"N":"2"},"Dimensions":{"S":"8.5x11.0x0.5"},"PageCount":{"N":"500"},"InPublication":{"B":"true"},"ProductCategory":{"S":"Book"}}' aws dynamodb put-item \ --table-name ProductCatalog \ --item '{"Id":{"N":"102"},"Title":{"S":"Book102Title"},"ISBN":{"S":"222-2222222222"},"Authors":{"SS":["Author1","Author2"]},"Price":{"N":"20"},"Dimensions":{"S":"8.5x11.0x0.8"},"PageCount":{"N":"600"},"InPublication":{"B":"true"},"ProductCategory":{"S":"Book"}}' aws dynamodb put-item \ --table-name ProductCatalog \ --item '{"Id":{"N":"103"},"Title":{"S":"Book103Title"},"ISBN":{"S":"333-3333333333"},"Authors":{"SS":["Author1","Author2"]},"Price":{"N":"2000"},"Dimensions":{"S":"8.5x11.0x1.5"},"PageCount":{"N":"600"},"InPublication":{"B":"false"},"ProductCategory":{"S":"Book"}}' aws dynamodb put-item \ --table-name ProductCatalog \ --item '{"Id":{"N":"201"},"Title":{"S":"18-Bike-201"},"Description":{"S":"201Description"},"BicycleType":{"S":"Road"},"Brand":{"S":"MountainA"},"Price":{"N":"100"},"Gender":{"S":"M"},"Color":{"SS":["Red","Black"]},"ProductCategory":{"S":"Bicycle"}}' aws dynamodb put-item \ --table-name ProductCatalog \ --item '{"Id":{"N":"202"},"Title":{"S":"21-Bike-202"},"Description":{"S":"202Description"},"BicycleType":{"S":"Road"},"Brand":{"S":"Brand-CompanyA"},"Price":{"N":"200"},"Gender":{"S":"M"},"Color":{"SS":["Green","Black"]},"ProductCategory":{"S":"Bicycle"}}' aws dynamodb put-item \ --table-name ProductCatalog \ --item '{"Id":{"N":"203"},"Title":{"S":"19-Bike-203"},"Description":{"S":"203Description"},"BicycleType":{"S":"Road"},"Brand":{"S":"Brand-CompanyB"},"Price":{"N":"300"},"Gender":{"S":"W"},"Color":{"SS":["Red","Green","Black"]},"ProductCategory":{"S":"Bicycle"}}' aws dynamodb put-item \ --table-name ProductCatalog \ --item '{"Id":{"N":"204"},"Title":{"S":"18-Bike-204"},"Description":{"S":"204Description"},"BicycleType":{"S":"Mountain"},"Brand":{"S":"Brand-CompanyB"},"Price":{"N":"400"},"Gender":{"S":"W"},"Color":{"SS":["Red"]},"ProductCategory":{"S":"Bicycle"}}' aws dynamodb put-item \ --table-name ProductCatalog \ --item '{"Id":{"N":"205"},"Title":{"S":"20-Bike-205"},"Description":{"S":"205Description"},"BicycleType":{"S":"Hybrid"},"Brand":{"S":"Brand-CompanyC"},"Price":{"N":"500"},"Gender":{"S":"B"},"Color":{"SS":["Red","Black"]},"ProductCategory":{"S":"Bicycle"}}'
色々試した後は、下記のコマンドでテーブルを削除することにより、不要な課金を抑えることができます。
aws dynamodb delete-table --table-name Forum aws dynamodb delete-table --table-name Thread aws dynamodb delete-table --table-name Reply aws dynamodb delete-table --table-name ProductCatalog